POST /promoter-association/curator/affiliate-link
[!summary] 接口概述 由帖子所有者(Curator)为其他推广者创建联盟推广链接,使其能够转发销售该帖子的产品。此接口会自动创建或查找推广者账户,并返回用于生成推广链接的关键信息。
接口功能说明
此接口用于 帖子所有者(Curator/Post Owner) 为其他用户创建联盟推广链接。主要功能包括:
- 自动用户管理:根据手机号或邮箱查找已验证用户,若不存在则自动创建联盟推广者账户
- 权限验证:仅帖子所有者可为其帖子创建推广链接
- 业务规则验证:
- 防止为自己的帖子创建推广链接
- 防止已在销售链中的用户重复参与
- 防止产品所有者参与推广
- 推广链接生成:返回
affiliateCode用于构造推广链接
请求地址 & 请求方式
POST /promoter-association/curator/affiliate-link
环境地址:
- 生产环境:
https://release.katana-api.1m.app - 开发环境:根据实际配置
请求头(Header)
| Header 名称 | 值示例 | 必填 | 说明 |
|---|---|---|---|
Authorization |
Bearer eyJhbGci... |
✅ | JWT 认证令牌 |
Content-Type |
application/json |
✅ | 请求内容类型 |
from |
client |
- | 请求来源标识 |
x-track-id |
UUID | - | 请求追踪 ID |
timezone |
Asia/Shanghai |
- | 时区信息 |
请求参数
Body 参数(JSON)
| 字段名 | 类型 | 必填 | 说明 | 验证规则 |
|---|---|---|---|---|
curatorId |
string (UUID) | ✅ | 帖子所有者的用户 ID | 必须是当前登录用户的 ID |
postAlias |
string | ✅ | 帖子的 URL 别名 | 帖子必须允许转发销售 (allowPromotersResell: true) |
phoneNumberOrEmail |
string | ✅ | 推广者的手机号或邮箱 | 必须是有效的手机号或邮箱格式 |
请求示例
{
"curatorId": "1ee2b015-5390-44ba-a677-8cbd53e8066f",
"postAlias": "000011",
"phoneNumberOrEmail": "neo.wang.ext+21@1m.app"
}
响应结构 & 字段说明
响应结构
{
"code": number, // HTTP 状态码
"message": string, // 响应消息
"request_id": string, // 请求追踪 ID
"data": {
"curator": {
"id": string, // 帖子所有者用户 ID
"vanityUrl": string // 帖子所有者的个性 URL
},
"post": {
"id": string, // 帖子 ID
"alias": string // 帖子别名
},
"promoter": {
"id": string, // 推广者用户 ID
"email": string, // 推广者邮箱(可能为 null)
"phoneNumber": string, // 推广者手机号(可能为 null)
"vanityUrl": string // 推广者个性 URL(可能为 null)
},
"affiliateCode": string // 推广者的联盟推广代码
}
}
字段说明
| 路径 | 字段 | 类型 | 说明 | |
|---|---|---|---|---|
data.curator.id |
帖子所有者 ID | string (UUID) | 创建推广链接的用户 ID | |
data.curator.vanityUrl |
帖子所有者个性 URL | string | 用于构建帖子所有者的店铺链接 | |
data.post.id |
帖子 ID | string (UUID) | 被推广的帖子 ID | |
data.post.alias |
帖子别名 | string | 帖子的 URL 别名 | |
data.promoter.id |
推广者 ID | string (UUID) | 推广者的用户 ID | |
data.promoter.email |
推广者邮箱 | string \ | null | 推广者的注册邮箱 |
data.promoter.phoneNumber |
推广者手机号 | string \ | null | 推广者的注册手机号 |
data.promoter.vanityUrl |
推广者个性 URL | string \ | null | 推广者的店铺链接前缀 |
data.affiliateCode |
推广代码 | string | 用于生成推广链接的唯一代码 |
成功示例
HTTP 200 成功响应
{
"code": 200,
"message": "success",
"request_id": "783af55d-5ff4-4874-8ff3-0e0482e09725",
"data": {
"curator": {
"id": "1ee2b015-5390-44ba-a677-8cbd53e8066f",
"vanityUrl": "neo"
},
"post": {
"id": "1316f4a1-f5c7-465d-bb59-aaef9bbcbd47",
"alias": "000011"
},
"promoter": {
"id": "310ed6c9-dbf9-4b44-817d-54e8a940d4e8",
"email": "neo.wang.ext+21@1m.app",
"phoneNumber": null,
"vanityUrl": null
},
"affiliateCode": "ehf843"
}
}
错误示例
401 Unauthorized - 未认证
{
"statusCode": 401,
"message": "Unauthorized"
}
场景:请求头中缺少有效的 Authorization 令牌。
400 Bad Request - 无权限操作
{
"statusCode": 400,
"message": "You do not have the permission to perform this operation.",
"error": "Bad Request"
}
场景:curatorId 与当前登录用户 ID 不匹配,即用户无权为他人创建推广链接。
400 Bad Request - 手机号或邮箱格式无效
{
"statusCode": 400,
"message": "PhoneNumber or email is invalid.",
"error": "Bad Request"
}
场景:phoneNumberOrEmail 参数既不是有效的手机号格式,也不是有效的邮箱格式。
400 Bad Request - 不能为自己创建推广链接
{
"statusCode": 400,
"message": "Can not create affiliate-link for the post owner.",
"error": "Bad Request"
}
场景:推广者(根据手机号/邮箱查找的用户)就是帖子所有者本人。
400 Bad Request - 推广者已在销售链中
{
"statusCode": 400,
"message": "The promoter has already resold this post",
"error": "Bad Request"
}
场景:推广者已经在这个帖子的销售链中(其 ID 存在于 post.uplineCreatorList)。
[!info] 业务逻辑 为防止用户在单个销售链中重复出现,系统会检查推广者是否已参与该帖子的转售。这可以避免佣金计算混乱和循环引用问题。
400 Bad Request - 不能为产品所有者创建推广链接
{
"statusCode": 400,
"message": "Can not create affiliate-link for the product owner",
"error": "Bad Request"
}
场景:推广者是帖子中关联产品的所有者。
[!info] 业务逻辑 产品所有者不能作为自己产品的推广者,以避免业务逻辑冲突。
404 Not Found - 帖子所有者或帖子不存在
{
"statusCode": 404,
"message": "Resource not found",
"error": "Not Found"
}
场景:
curatorId对应的用户不存在postAlias对应的帖子不存在- 帖子存在但
allowPromotersResell为false
注意事项 & 业务逻辑
认证与授权
[!warning] 认证要求 此接口 必须认证,请求头中必须包含有效的
Authorization: Bearer <token>。[!warning] 权限验证 只有帖子的 所有者 才能为其帖子创建推广链接。系统会验证
curatorId必须与当前登录用户 ID 一致。
用户创建逻辑
[!important] 自动创建推广者账户 当根据
phoneNumberOrEmail查找不到用户时,系统会自动创建一个新的联盟推广者账户:
- 用户角色:
PROMOTER- 用户类型:
AFFILIATE- 邮箱:若提供邮箱,则设置为该邮箱
- 手机号:若提供手机号,则设置为该手机号
[!note] 查找用户优先级 系统使用
findAffiliateOrVerifiedUserByPhoneNumberOrEmail方法查找用户,优先返回:
- 联盟类型(
AFFILIATE)用户- 已验证手机号或邮箱的标准用户
帖子验证规则
[!warning] 帖子必须允许转发 系统会验证帖子的
allowPromotersResell字段必须为true,否则返回 404 错误。
销售链防重逻辑
[!important] 防止销售链重复 系统会检查以下情况以防止用户在销售链中重复出现:
- 帖子转售检查:检查推广者 ID 是否在
post.uplineCreatorList中- 产品所有者检查:检查推广者 ID 是否为
post.relatedProducts中任何产品的所有者违反任一规则将返回 400 错误。
推广链接构造
[!info] 推广链接格式 虽然此接口仅返回
affiliateCode,但完整的推广链接通常格式为:https://<subdomain>/p/<curator_vanity_url>/<post_alias>?ref=<affiliate_code>例如:
https://release.pear.us/p/neo/000011?ref=ehf843
相关接口
| 接口 | 说明 |
|---|---|
POST /promoter-association/guest/affiliate-link |
公开接口,访客也可创建推广链接 |
GET /promoter-association/affiliate-promoters |
获取已创建的推广者列表 |
POST /promoter-association/affiliate-link/notification |
发送推广链接创建通知(邮件/短信) |
使用场景
- Curator 主动邀请:帖子所有者通过输入手机号/邮箱为特定用户创建推广权限
- 批量推广:Curator 为多个推广者批量创建推广链接
- 新用户转化:非注册用户通过此接口自动注册为联盟推广者
时区处理
[!note] 时区支持 请求头中的
timezone参数会影响某些时间相关的计算,但此接口主要返回用户信息,时间戳使用 UTC 标准。
数据流与泳道图
系统架构概览
┌─────────────────────────────────────────────────────────────────────────────┐
│ 系统分层架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ Client Layer │ Controller Layer │ Service Layer │ Database │
│ (Frontend/Mobile) │ (NestJS) │ (Business) │ (Prisma) │
└─────────────────────────────────────────────────────────────────────────────┘
完整数据流泳道图
sequenceDiagram
autonumber
participant Client as 客户端<br/>(Frontend)
participant Controller as Controller<br/>PromoterAssociationController
participant Service as Service<br/>PromoterAssociationService
participant UserRepo as UserMetaRepository
participant PARepo as PromoterAssociationRepository
participant DB as Database<br/>(PostgreSQL)
Note over Client,DB: === 阶段1: 请求验证与参数解析 ===
Client->>Controller: POST /promoter-association/curator/affiliate-link<br/>{curatorId, postAlias, phoneNumberOrEmail}
Controller->>Controller: 从 JWT Token 获取 userId
Controller->>Controller: 验证 curatorId === userId
alt 权限验证失败
Controller-->>Client: 400 Bad Request<br/>"You do not have the permission..."
end
Controller->>Controller: 验证 phoneNumberOrEmail 格式<br/>(isEmail() || isPhoneNumber())
alt 格式验证失败
Controller-->>Client: 400 Bad Request<br/>"PhoneNumber or email is invalid."
end
Note over Client,DB: === 阶段2: 并行查询数据 ===
par 并行查询三个数据源
Controller->>UserRepo: findUserEntityById(curatorId)
UserRepo->>DB: SELECT * FROM "User"<br/>WHERE id = curatorId
DB-->>UserRepo: curator (UserEntity)
UserRepo-->>Controller: curator
and
Controller->>PARepo: findPostByAliasAndCreatorId()<br/>{urlAlias, creatorId, allowPromotersResell}
PARepo->>DB: SELECT * FROM "Post"<br/>WHERE urlAlias = postAlias<br/>AND creatorId = curatorId<br/>AND allowPromotersResell = true
DB-->>PARepo: post (Post)
PARepo-->>Controller: post
and
Controller->>UserRepo: findAffiliateOrVerifiedUserByPhoneNumberOrEmail()
UserRepo->>DB: SELECT * FROM "User"<br/>WHERE (email = input OR phoneNumber = input)<br/>AND (type = 'AFFILIATE' OR emailVerified = true<br/>OR phoneNumberVerified = true)<br/>ORDER BY createdAt DESC
DB-->>UserRepo: promoter (UserEntity | null)
UserRepo-->>Controller: promoter
end
Note over Client,DB: === 阶段3: 业务规则验证 ===
Controller->>Controller: 检查 curator && post 是否存在
alt curator 或 post 不存在
Controller-->>Client: 404 Not Found<br/>"Resource not found"
end
Controller->>Controller: 检查 promoter.userId !== curator.userId
alt promoter 就是 curator
Controller-->>Client: 400 Bad Request<br/>"Can not create affiliate-link<br/>for the post owner."
end
Controller->>Controller: 检查 promoter.userId 不在<br/>post.uplineCreatorList 中
alt promoter 已在销售链中
Controller-->>Client: 400 Bad Request<br/>"The promoter has already<br/>resold this post"
end
Controller->>Controller: 检查 promoter.userId 不是<br/>post.relatedProducts 的所有者
alt promoter 是产品所有者
Controller-->>Client: 400 Bad Request<br/>"Can not create affiliate-link<br/>for the product owner"
end
Note over Client,DB: === 阶段4: 用户创建或复用 ===
alt promoter 不存在
Controller->>Service: createGuestUser()<br/>{promoterEmail, promoterPhoneNumber}
Service->>DB: INSERT INTO "User"<br/>(id, email, phoneNumber,<br/>userRole=CONSUMER, type=GUEST,<br/>createdFeature=AFFILIATE)
DB-->>Service: newUser
Service-->>Controller: promoterUserEntity
else promoter 已存在
Note over Controller: 复用现有 promoter
end
Note over Client,DB: === 阶段5: 构造响应 ===
Controller->>Controller: toCreateAffiliateLinkResponse()<br/>{curator, promoter, post}
Controller-->>Client: 200 OK<br/>{curator, post, promoter, affiliateCode}
涉及的数据表
| 表名 | 操作类型 | 说明 |
|---|---|---|
User |
SELECT | 查询 Curator 信息 |
User |
SELECT | 查询 Promoter 信息(根据 phone/email) |
User |
INSERT (可选) | 若 Promoter 不存在,创建新用户 |
Post |
SELECT | 查询帖子信息并验证 allowPromotersResell |
PromoterAssociation |
INSERT/UPDATE | (新增) 创建或更新推广关联记录 |
[!info] ⚠️ PromoterAssociation 表的使用 此接口现在会创建
PromoterAssociation记录(2026-02-26 更新)记录内容:
invitationType = 'AFFILIATE'- 标识为联盟推广类型note = '{"postId": "xxx", "createdAt": "..."}'- 存储帖子元数据allowSyncPosts = false- 联盟推广者默认不同步帖子目的:
- 跟踪哪些推广者收到了哪个帖子的联盟链接
- 支持帖子所有者查看已发送的推广链接
- 区分"已发送链接"和"已建立合作关系"
[!tip] 数据库查询优化
- 并行查询:阶段 2 中的三个查询(Curator、Post、Promoter)并行执行,减少响应时间
- 索引使用:查询使用
id、urlAlias + creatorId等索引字段
关键业务节点说明
| 阶段 | 关键逻辑 | 代码位置 |
|---|---|---|
| 权限验证 | userId !== curatorId 抛出异常 |
Controller:296-301 |
| 格式验证 | isEmail() 或 isPhoneNumber() |
Controller:303-306 |
| 帖子查询 | allowPromotersResell: true 必须满足 |
Repo:326-329 |
| 用户查询优先级 | AFFILIATE 类型 > 已验证用户 | Repo:246-248 |
| 销售链防重 | 检查 uplineCreatorList 和 relatedProducts |
Controller:333-346 |
| 用户创建 | userRole=CONSUMER, type=GUEST |
Service:313-323 |
数据流转图
graph LR
subgraph Request["请求输入"]
A[CuratorID]
B[PostAlias]
C[Phone/Email]
end
subgraph Validation["验证层"]
D{JWT认证}
E{格式验证}
F{权限验证}
end
subgraph Query["查询层"]
G[(User表<br/>Curator)]
H[(Post表)]
I[(User表<br/>Promoter)]
end
subgraph BusinessRules["业务规则"]
J{不是自己?}
K{不在销售链?}
L{不是产品主?}
end
subgraph UserCreation["用户处理"]
M{Promoter<br/>存在?}
N[创建新用户<br/>GUEST/AFFILIATE]
O[复用现有用户]
end
subgraph Response["响应输出"]
P[Curator信息]
Q[Post信息]
R[Promoter信息]
S[AffiliateCode]
end
A --> D
B --> D
C --> E
D --> F
E --> F
F --> G
F --> H
F --> I
G --> J
H --> K
I --> L
J --> M
K --> M
L --> M
M -->|否| N
M -->|是| O
N --> P
O --> P
G --> P
H --> Q
I --> R
R --> S
style Request fill:#e1f5fe
style Validation fill:#fff3e0
style Query fill:#f3e5f5
style BusinessRules fill:#ffebee
style UserCreation fill:#e8f5e9
style Response fill:#e0f2f1
PromoterAssociation 表的生命周期
[!important] ⚠️ 重要更新 自 2026-02-26 起,
createAffiliateLink接口现在会创建PromoterAssociation记录
- 此记录的
invitationType = AFFILIATEnote字段存储 JSON 元数据:{"postId": "xxx", "createdAt": "2026-02-26T..."}- 这使得帖子所有者可以查看他们为每个帖子创建的所有联盟链接
以下是 PromoterAssociation 记录的完整生命周期:
graph LR
subgraph Phase1["阶段1: 创建推广链接 (本接口)"]
A[POST /curator/affiliate-link]
B["验证: User + Post"]
C["确保 Promoter 存在"]
D["创建/更新 PromoterAssociation"]
B --> C
C --> D
D --> E["✅ invitationType=AFFILIATE<br/>note={postId, createdAt}"]
end
subgraph Phase2["阶段2: 查询推广链接 (新接口)"]
F["GET /curator/affiliate-links"]
G["按 curatorId + postId 过滤"]
F --> G
G --> H["返回所有推广者列表"]
end
subgraph Phase3["阶段3: 接受邀请 (其他接口)"]
I["用户注册/下单"]
J["POST /accept-invitation"]
K["更新为 INVITATION_LINK"]
I --> J
J --> K
end
Phase1 --> Phase2
Phase2 --> Phase3
style Phase1 fill:#e3f2fd
style Phase2 fill:#fff3e0
style Phase3 fill:#e8f5e9
style E fill:#ffcdd2
style H fill:#ffcdd2
style K fill:#c8e6c9
| 阶段 | 接口/操作 | 涉及表 | PromoterAssociation 状态 |
|---|---|---|---|
| 1. 准备链接 | POST /curator/affiliate-link |
User, Post |
❌ 不创建 |
| 2. 发送链接 | POST /affiliate-link/notification |
无变更 | ❌ 不创建 |
| 3. 用户访问 | 前端解析 ?ref=xxx |
无变更 | ❌ 不创建 |
| 4. 用户注册 | 用户通过链接注册 | User (创建) |
❌ 不创建 |
| 5. 接受邀请 | POST /accept-invitation |
PromoterAssociation |
✅ 在此创建 |
| 6. 首次下单 | 下单时自动关联 | PromoterAssociation |
✅ 可能在此创建 |
[!tip] 设计理由 为什么不在本接口创建 PromoterAssociation?
- 避免垃圾数据:用户可能从未点击链接或注册
- 状态区分:区分"已发送邀请"和"已建立合作"
- 业务语义:推广链接只是"邀请函",真正接受邀请才算"关联"
- 性能优化:减少无效关联记录的存储和查询
相关代码文件
| 文件路径 | 说明 |
|---|---|
src/promoter-association/promoter-association.controller.ts |
控制器,定义接口路由和请求处理逻辑 (行 290-368) |
src/promoter-association/promoter-association.interface.ts |
接口定义,包含 DTO 和响应类型 |
src/promoter-association/promoter-association.service.ts |
服务层,实现 createGuestUser() 等业务逻辑 |
src/promoter-association/promoter-association.repo.ts |
数据访问层,实现 findPostByAliasAndCreatorId() 等 |
src/user-meta/userMeta.repository.ts |
用户元数据仓库,实现 findAffiliateOrVerifiedUserByPhoneNumberOrEmail() |
GET /promoter-association/curator/affiliate-links
[!summary] 新增接口 - 查询帖子的联盟推广链接 帖子所有者可以查看他们为特定帖子创建的所有联盟推广链接记录。
新增日期: 2026-02-26
接口功能说明
此接口用于 帖子所有者(Curator) 查询他们为特定帖子创建的所有联盟推广链接。主要功能包括:
- 按帖子过滤:可查询特定帖子的所有联盟链接,或查询所有帖子的链接
- 推广者信息:返回每个推广者的详细信息(姓名、邮箱、手机号、个性 URL、Logo)
- 分页支持:支持分页查询
- 搜索功能:可根据推广者姓名、邮箱、手机号搜索
- 帖子信息:返回帖子标题、封面图等基本信息
请求地址 & 请求方式
GET /promoter-association/curator/affiliate-links
请求头(Header)
| Header 名称 | 值示例 | 必填 | 说明 |
|---|---|---|---|
Authorization |
Bearer eyJhbGci... |
✅ | JWT 认证令牌 |
请求参数(Query)
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
postAlias |
string | ❌ | 帖子别名,用于过滤特定帖子的链接(不传则返回所有帖子的链接) |
请求示例
GET /promoter-association/curator/affiliate-links?postAlias=000011
GET /promoter-association/curator/affiliate-links
[!note] 无分页参数 此接口不支持分页,返回所有符合条件的记录。
响应结构 & 字段说明
响应结构
[
{
"id": string, // PromoterAssociation ID
"postId": string, // 帖子 ID
"postAlias": string, // 帖子别名
"postTitle": string | null,// 帖子标题
"postCoverImage": string | null, // 帖子封面图
"promoter": {
"id": string, // 推广者用户 ID
"email": string | null, // 推广者邮箱
"phoneNumber": string | null, // 推广者手机号
"fullName": string, // 推广者姓名
"vanityUrl": string | null, // 推广者个性 URL
"logo": string | null, // 推广者头像
"affiliateCode": string // 推广代码
},
"createdAt": string, // 创建时间 (ISO 8601)
"invitationType": string // 邀请类型 (枚举值: "AFFILIATE" | "INVITATION_LINK")
}
]
成功示例
HTTP 200 成功响应
[
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"postId": "1316f4a1-f5c7-465d-bb59-aaef9bbcbd47",
"postAlias": "000011",
"postTitle": "Summer Collection 2024",
"postCoverImage": "https://cdn.example.com/cover.jpg",
"promoter": {
"id": "310ed6c9-dbf9-4b44-817d-54e8a940d4e8",
"email": "neo.wang.ext+21@1m.app",
"phoneNumber": null,
"fullName": "Neo Wang",
"vanityUrl": "neo-wang",
"logo": "https://cdn.example.com/avatar.jpg",
"affiliateCode": "ehf843"
},
"createdAt": "2026-02-26T13:45:00.000Z",
"invitationType": "AFFILIATE"
}
]
错误示例
404 Not Found - 帖子不存在
{
"statusCode": 404,
"message": "Resource not found",
"error": "Not Found"
}
场景:指定的 postAlias 不存在或不属于当前用户。
注意事项 & 业务逻辑
数据来源
[!info] 数据来源 此接口查询的数据来自
PromoterAssociation表,条件为:
curatorId = 当前用户 IDinvitationType = 'AFFILIATE'deleted_at IS NULL帖子 ID 从
note字段的 JSON 元数据中解析:{"postId": "xxx", "createdAt": "..."}
与创建接口的配合
[!tip] 配合使用
- 调用
POST /curator/affiliate-link创建联盟链接 → 自动创建 PromoterAssociation 记录- 调用
GET /curator/affiliate-links查询已创建的所有链接
业务规则
- 只返回自己创建的链接:只能查询自己作为 Curator 创建的联盟链接
- 帖子过滤:指定
postAlias只返回该帖子的链接 - 去重逻辑:同一 Curator-Promoter 组合只保留一条记录(通过
deletedAt软删除) - 按创建时间倒序:最新创建的链接排在前面
- 无分页:一次性返回所有符合条件的记录
相关代码文件
| 文件路径 | 说明 |
|---|---|
src/promoter-association/promoter-association.controller.ts |
控制器,新增 getPostAffiliateLinks 方法 |
src/promoter-association/promoter-association.service.ts |
服务层,实现 getPostAffiliateLinks 和 createAffiliateAssociationWithPost 方法 |
src/promoter-association/promoter-association.repo.ts |
数据访问层,实现 findAffiliateAssociationsWithPromoterInfo 方法 |
src/promoter-association/promoter-association.interface.ts |
接口定义,新增 GetPostAffiliateLinksQuery 和 PostAffiliateLinkResponse |
修改记录
| 日期 | 修改内容 |
|---|---|
| 2026-02-26 | 新增接口:查询帖子的联盟推广链接 |
| 2026-02-26 | 重要变更:POST /curator/affiliate-link 现在会创建 PromoterAssociation 记录 |